<?php
namespace Lib\Http;

use Core\HHO;

/**
 * Cookie 管理器，管理机制参照了 Firefox 浏览器。
 *
 * @author SUSISOY
 *
 */
final class Cookie {
	/**
	 * Cookie 数据的持久化方式配置参数名称。
	 * @var int
	 */
	const OPT_STORAGE = 1;
	/**
	 * Cookie 数据的最大字节数，超过此值将禁止被持久化，默认为 1M。
	 * @var int
	 */
	const OPT_MAX_SIZE = 2;
	
	/**
	 * 关闭 Cookie 数据的持久化。
	 * @var int
	 */
	const COOKIE_STORAGE_CLOSE = 0;
	/**
	 * Cookie 数据使用文件进行持久化。
	 * @var int
	 */
	const COOKIE_STORAGE_FILES = 1;
	
	/**
	 * 当前 Cookie 管理器的默认域名。
	 * @var string
	 */
	private $_domain = '';
	/**
	 * 当前 Cookie 管理器的默认路径。
	 * @var string
	 */
	private $_path = '';
	/**
	 * 当前 Cookie 管理器中各域名的会话ID。
	 * @var string
	 */
	private $_session = '';
	/**
	 * 当前 Cookie 管理器中的 Cookie 变量列表。
	 * @var array
	 */
	private $_cookie = [];
	/**
	 * 当前 Cookie 管理器的配置参数信息。
	 * @var array
	 */
	private $_option = [self::OPT_STORAGE => self::COOKIE_STORAGE_CLOSE, self::OPT_MAX_SIZE => 1048576];

	/**
	 * $session 参数允许同一域名下维持多个会话。
	 *
	 * @param string $domain
	 * @param string $path
	 * @param string $session
	 * @return void
	 */
	private function __construct($domain, $path, $session) {
		$this->_domain = $domain ?  : '*';
		
		$this->_path = $path ?  : '/';
		
		$this->_session = $this->_domain != '*' && $session ? $session : '';
	}

	/**
	 * 设置或获得当前 Cookie 管理器的配置参数值，返回值如下：
	 * - 设置成功返回 TRUE，设置失败返回 FALSE。
	 * - 获得成功返回指定配置参数的值，失败返回 NULL。
	 *
	 * @param int $name
	 * @param mixed $value
	 * @return mixed
	 */
	public function option($name, $value = NULL) {
		if (is_null($value)) {
			return isset($this->_option[$name]) ? $this->_option[$name] : NULL;
		}
		
		switch ($name) {
			case self::OPT_STORAGE :
				if (!in_array($value, [self::COOKIE_STORAGE_CLOSE, self::COOKIE_STORAGE_FILES])) {
					return FALSE;
				}
				
				// 未初始化默认域名时给予警告，但还允许设置持久化，因为当前管理器中可能托管有其它域名的 Cookie。
				if ($this->_domain == '*' && $value != self::COOKIE_STORAGE_CLOSE) {
					trigger_error('Unknown domain name can not open cookie data persistence', E_USER_WARNING);
				}
				
				break;
			case self::OPT_MAX_SIZE :
				if ($value <= 0) {
					return FALSE;
				}
				
				break;
			default :
				return FALSE;
		}
		
		$this->_option[$name] = $value;
		
		return TRUE;
	}

	/**
	 * 处理要设置的 Cookie 变量的一些信息。
	 *
	 * @param string &$domain
	 * @param string &$path
	 * @param int|string &$expire
	 * @return boolean
	 */
	private function _tidy(&$domain, &$path, &$expire = NULL) {
		$domain = $domain ? strtolower(ltrim($domain, '.')) : $this->_domain;
		
		$path = $path ? (rtrim($path, '/') ?  : '/') : $this->_path;
		if ($path[0] != '/') {
			return FALSE;
		}
		
		$expire = empty($expire) ? PHP_INT_MAX : (is_numeric($expire) ? intval($expire) : strtotime($expire));
		$expire = $expire ?  : PHP_INT_MAX;
		
		return TRUE;
	}

	/**
	 * 重载指定域名下的 Cookie 变量。
	 *
	 * @param string $domain
	 * @return boolean
	 */
	private function _load($domain) {
		if ($domain != '*') {
			if ($this->_option[self::OPT_STORAGE] == self::COOKIE_STORAGE_FILES) {
				$files = HHO::$config['runtime.root'] . '/Lib/Http/Cookie/' . $domain . '-' . $this->_session . '.txt';
				
				if (is_file($files)) {
					$data = file_get_contents($files);
					if ($data === FALSE) {
						return FALSE;
					}
					
					$this->_cookie[$domain] = unserialize($data);
				} elseif (isset($this->_cookie[$domain])) {
					unset($this->_cookie[$domain]);
				}
			}
		}
		
		return TRUE;
	}

	/**
	 * 保存指定域名下的 Cookie 变量。
	 *
	 * @param string $domain
	 * @return boolean
	 */
	private function _keep($domain) {
		if ($domain != '*') {
			if ($this->_option[self::OPT_STORAGE] == self::COOKIE_STORAGE_FILES) {
				$files = HHO::$config['runtime.root'] . '/Lib/Http/Cookie/' . $domain . '-' . $this->_session . '.txt';
				
				if (!isset($this->_cookie[$domain])) {
					return unlink($files);
				}
				
				$data = serialize($this->_cookie[$domain]);
				
				if (strlen($data) > $this->_option[self::OPT_MAX_SIZE]) {
					trigger_error('Cookie data size exceeds the limit', E_USER_WARNING);
					
					return FALSE;
				}
				
				if ($this->_option[self::OPT_STORAGE] == self::COOKIE_STORAGE_FILES) {
					$dir = HHO::$config['runtime.root'] . '/Lib/Http/Cookie';
					
					is_dir($dir) || mkdir($dir, NULL, TRUE);
					
					return file_put_contents($files, $data) != FALSE;
				}
			}
		}
		
		return TRUE;
	}

	/**
	 * 设置一个 Cookie 变量，并保存。
	 *
	 * @param string $name
	 * @param mixed $value
	 * @param int $expire
	 * @param string $path
	 * @param string $domain
	 * @return boolean
	 */
	private function _cookie($name, $value, $expire, $path, $domain) {
		if (isset($value) && $expire > time()) {
			$this->_cookie[$domain][$path][$name] = ['value' => (string) $value, 'expire' => $expire];
		} elseif (isset($this->_cookie[$domain][$path][$name])) {
			unset($this->_cookie[$domain][$path][$name]);
			
			if (count($this->_cookie[$domain][$path]) == 0) {
				unset($this->_cookie[$domain][$path]);
			}
			
			if (count($this->_cookie[$domain]) == 0) {
				unset($this->_cookie[$domain]);
			}
		} else {
			return TRUE;
		}
		
		return $this->_keep($domain);
	}

	/**
	 * 设置一个 Cookie 变量。
	 *
	 * @param string $name
	 * @param mixed $value
	 * @param int|string $expire
	 * @param string $path
	 * @param string $domain
	 * @return boolean
	 */
	public function set($name, $value = NULL, $expire = NULL, $path = NULL, $domain = NULL) {
		if ($this->_tidy($domain, $path, $expire) && $this->_load($domain)) {
			return $this->_cookie($name, $value, $expire, $path, $domain);
		}
		
		return FALSE;
	}

	/**
	 * 参见 self::set() 方法。
	 *
	 * @param string $name
	 * @param mixed $value
	 * @return void
	 */
	public function __set($name, $value) {
		$this->set($name, $value);
	}

	/**
	 * 删除一个 Cookie 变量，使用 self::set() 方法也可实现删除一个 Cookie 变量，参见 self::set() 方法。
	 *
	 * @param string $name
	 * @return void
	 */
	public function __unset($name) {
		$this->set($name);
	}

	/**
	 * 解析一条 HTTP 消息的 Set-Cookie 头。
	 *
	 * @param string $data
	 * @param string $path
	 * @param string $domain
	 * @return boolean
	 */
	public function setcookie($data, $path = NULL, $domain = NULL) {
		$cookie = explode(';', $data);
		
		if (!preg_match('/^([^=]+)=(.*)$/', trim(array_shift($cookie)), $var)) {
			return FALSE;
		}
		
		$other = ['expires' => 0, 'path' => '', 'domain' => ''];
		foreach ($cookie as $v) {
			$o = explode('=', trim($v));
			
			if (strcasecmp($o[0], 'HttpOnly') == 0) {
				continue;
			}
			
			$other[strtolower(trim($o[0]))] = trim($o[1]);
		}
		
		// ================================================================
		$domain = $other['domain'] ?  : $domain;
		$path = $other['path'] ?  : $path;
		
		if (!$this->_tidy($domain, $path, $other['expires'])) {
			return FALSE;
		}
		
		if (!$this->_load($domain)) {
			return FALSE;
		}
		
		return $this->_cookie($var[1], $var[2], $other['expires'], $path, $domain);
	}

	/**
	 * 获得 Cookie 变量，其中包括变量值和过期时间，返回值数据结构如下：
	 * - 指定 $name 为字符串时，成功则返回一个一维数组。
	 * - 未指定 $name 或指定 $name 为一个数组时，成功则返回一个二维数组。
	 * - 失败则返回 FALSE。
	 * - 不存在 Cookie 变量，则返回一个空数组。
	 *
	 * @param string|array $name
	 * @param string $path
	 * @param string $domain
	 * @return array|FALSE
	 */
	public function get($name = NULL, $path = NULL, $domain = NULL) {
		if (!$this->_tidy($domain, $path)) {
			return FALSE;
		}
		
		// ================================================================
		$domains = array();
		
		$host = preg_match('/^\d+\.\d+\.\d+\.\d+$/', $domain) ? [$domain] : explode('.', $domain);
		
		$count = count($host);
		for ($i = ($count == 1 ? 1 : 2); $i <= $count; $i++) {
			$domains[] = implode('.', array_slice($host, 0 - $i));
		}
		
		// ================================================================
		$paths = array($path);
		
		while ($path != '/') {
			$path = str_replace('\\', '/', dirname($path));
			
			$paths[] = $path;
		}
		
		$paths = array_reverse($paths);
		
		// ================================================================
		$cookies = array();
		
		$time = time();
		$names = is_array($name) ? $name : [$name];
		
		foreach ($domains as $domain) {
			if (!$this->_load($domain)) {
				return FALSE;
			}
			
			foreach ($paths as $p) {
				if (isset($this->_cookie[$domain][$p])) {
					foreach ($this->_cookie[$domain][$p] as $n => $c) {
						if ($c['expire'] < $time) {
							continue;
						}
						
						if (!$name || in_array($n, $names)) {
							$cookies[$n] = $c;
						}
					}
				}
			}
		}
		
		return is_string($name) && isset($cookies[$name]) ? $cookies[$name] : $cookies;
	}

	/**
	 * 参见 self::get() 方法。
	 *
	 * @param string $name
	 * @return string
	 */
	public function __get($name) {
		$cookie = $this->get($name);
		
		return isset($cookie['value']) ? $cookie['value'] : NULL;
	}

	/**
	 * 生成并返回 Cookie 变量的 HTTP 信息头。
	 *
	 * @param string $path
	 * @param string $domain
	 * @return string
	 */
	public function package($path = NULL, $domain = NULL) {
		$cookie = $this->get(NULL, $path, $domain);
		
		if (is_array($cookie)) {
			$list = [];
			
			foreach ($cookie as $n => $c) {
				$list[] = $n . '=' . $c['value'];
			}
			
			return implode('; ', $list);
		}
		
		return '';
	}

	/**
	 * 参见 self::package() 方法。
	 *
	 * @return string
	 */
	public function __toString() {
		return $this->package();
	}

	/**
	 * 清除指定域名的 Cookie 数据。
	 *
	 * @param string $domain
	 * @return boolean
	 */
	public function clear($domain = NULL) {
		$domain = $domain ? strtolower(ltrim($domain, '.')) : $this->_domain;
		
		if (isset($this->_cookie[$domain])) {
			unset($this->_cookie[$domain]);
			
			if ($domain != '*') {
				if ($this->_option[self::OPT_STORAGE] == self::COOKIE_STORAGE_FILES) {
					$files = HHO::$config['runtime.root'] . '/Lib/Http/Cookie/' . $domain . '-' . $this->_session . '.txt';
					
					return is_file($files) ? unlink($files) : TRUE;
				}
			}
		}
		
		return TRUE;
	}

	/**
	 * 清除当前 Cookie 管理器中所有域名的 Cookie 数据。
	 *
	 * @return boolean
	 */
	public function destruct() {
		if ($this->_option[self::OPT_STORAGE] == self::COOKIE_STORAGE_FILES) {
			$this->_load($this->_domain);
			
			foreach (array_keys($this->_cookie) as $domain) {
				$files = HHO::$config['runtime.root'] . '/Lib/Http/Cookie/' . $domain . '-' . $this->_session . '.txt';
				
				is_file($files) && unlink($files) || trigger_error('Cookie data file does not exist or can not be removed', E_USER_WARNING);
			}
		}
		
		$this->_cookie = [];
		
		return TRUE;
	}

	/**
	 * 销毁 Cookie 对象时，清理已过期 Cookie 变量。
	 *
	 * @return void
	 */
	public function __destruct() {
		$time = time();
		
		$keep = array();
		
		foreach ($this->_cookie as $domain => &$cookie) {
			foreach ($cookie as $path => &$variable) {
				foreach ($variable as $name => $var) {
					if ($var['expire'] < $time) {
						unset($variable[$name]);
						
						isset($keep[$domain]) || $keep[$domain] = TRUE;
					}
				}
				
				if (count($variable) == 0) {
					unset($cookie[$path]);
				}
			}
			
			if (count($this->_cookie[$domain]) == 0) {
				unset($this->_cookie[$domain]);
			}
		}
		
		foreach ($keep as $d) {
			$this->_keep($d);
		}
	}

	/**
	 * 获得指定URL（但不限于该URL）和会话ID的 Cookie 管理器（Cookie 对象）。
	 *
	 * @param string $url
	 * @param string $session
	 * @return Lib\Http\Cookie
	 */
	public static function getInstance($url = NULL, $session = NULL) {
		$uri = parse_url(preg_match('/^https?:\/\/\w/i', $url) ? $url : $url[0] == '/' ? $url : 'http://' . $url);
		
		$domain = isset($uri['host']) ? strtolower($uri['host']) : '';
		$path = isset($uri['path']) ? rtrim($uri['path'], '/') : '';
		
		if (!$domain) {
			return new self(NULL, $path, NULL);
		}
		
		// ================================================================
		static $Cookie = NULL;
		
		$id = $domain . ':' . $session;
		
		isset($Cookie[$id]) || $Cookie[$id] = new self($domain, $path, $session);
		
		return $Cookie[$id];
	}
}